关于西电一站式服务大厅背后的那点事

好吧,这个假期闲得慌想用 Flutter 把电表重写了(毕竟本人能力很差,就很容易瞎想)。目前开发还是没个身影,但是我感觉,我把核心功能给写出来了,就差套个壳。我觉得,核心功能就是从学校服务器上当下来我需要的数据。根据我项目的简介,应该是开发完大半了。

但是呢,鉴于本人的鸽子属性,我感觉我得在我将来大概率弃坑之前,留点材料,以方便将来有比我更无聊,更疯狂的人来完成他心目中的电表。

关于我校一站式服务大厅

我们学校的一站式服务大厅是由南京金智教育开发的,而且很多学校都用这个系统。所以,接下来的部分,感觉很多同志们都能适用。

我们学校的一站式大厅有很多功能,不过其中仅仅有很少的一部分我们能用。我感觉平常使用最多的就是:

而我们平常要使用一站式(或者其他东西,比如健康报告)的话,一般都要先登陆统一认证平台,之后才能进入一站式。而这个统一认证平台,也是金智的:-P

提前说明

  1. 以下使用的语言是 Dart,编写 Flutter 应用的语言。个人觉得还算好理解吧......
  2. 下面步骤,会多次出现本人故意暂停跳转情况发生。因为我发现,要是自动跳转,可能 Cookie 会存不下来,要是 python 的 request 库就没有这个问题。

如何登陆统一认证平台

工具

根据 xidian-script 姐妹计划 libxduauth 所说,登陆所需要的工具如下:

  1. Cookie Jar。
  2. 网络传输工具,其中需要设置 "application/x-www-form-urlencoded" 来传输参数。
  3. 网页分析工具,比如大名鼎鼎的 BeautifulSoup 库。

操作步骤

获取登陆网页

向 http://ids.xidian.edu.cn/authserver/login 发送 GET 请求,其中请求参数是:

{
    "service": 接下来要访问的网址,
    "type": "userNameLogin"
}

如果正常,会返回登录网页。

将登陆网页交给网页分析工具,让他找到网页中 id 为 pwdFromId 的元素们,这里记为 form:

/// Import 'package:beautiful_soup_dart/beautiful_soup.dart' before use.
var page = BeautifulSoup(response);
var form = page.find("form",attrs: {"id": "pwdFromId"});

检查是否需要验证码

向 http://ids.xidian.edu.cn/authserver/checkNeedCaptcha.htl 发送 GET 请求,请求参数是

{
    "username": 填入你的学号,
    "_":目前时间戳
}

查看返回值中有没有 true。

如果需要填入验证码的话,向 http://ids.xidian.edu.cn/authserver/getCaptcha.htl 获取验证码图片,请求参数是

{ 目前时间戳: "" }

加密密码

首先,我们需要填充密码。这里我们使用 PKCS7 填充方式。我的程序自己实现了,因为没看明白 Dart 里面 PKCS7 咋用的:-(:

然后,从 form 里面寻找 input 标签,且 id 为 pwdEncryptSalt 的元素,这里面是加密密码的密钥(还是盐啥的)。最后,使用 AES-CBC 算法加密,然后返回字符串。

上述步骤的具体代码如下:

/// Get base64 encoded data. Which is aes encrypted [toEnc] encoded string using [key].
/// Padding part is copied from libxduauth"s idea.
/// Import "package:encrypt/encrypt.dart" before use.
String aesEncrypt(String toEnc, String key) {
  dynamic k = Key.fromUtf8(key);
  var crypt = AES(k, mode: AESMode.cbc, padding: null);
  /// Start padding
  int blockSize = 16;
  List<int> dataToPad = [];
  dataToPad.addAll(utf8.encode("xidianscriptsxduxidianscriptsxduxidianscriptsxduxidianscriptsxdu$toEnc"));
  int paddingLength = blockSize - dataToPad.length % blockSize;
  for (var i = 0; i < paddingLength; ++i) {
    dataToPad.add(paddingLength);
  }
  String readyToEnc = utf8.decode(dataToPad);
  /// Start encrypt.
  return Encrypter(crypt)
      .encrypt(readyToEnc, iv: IV.fromUtf8("xidianscriptsxdu"))
      .base64;
}

发送登陆报文

首先,构建登陆请求所需要的头。初步需要三个信息:

在此之后,我们需要寻找很多预配置的头,加入到我们的头中。这些元素都在 form 中,是 input 标签 ,参数是 "type=hidden" 。循环将其插入到请求头中。

最后,向 http://ids.xidian.edu.cn/authserver/login 发送 POST 请求,其中请求头是上面搞过的请求头,请求参数如下,注意请求不要自动跳转:

{"service": 接下来要访问的网址}

如果正常,ids.xidian.edu.cn 下面的 Cookie 会多一个 "happyVoyagePersonal=...; Path=/personalinfo"。然后需要我们自己向需要跳转的新网址发 GET 请求,同样,不需要自动跳转。

到此,通过统一认证平台,我们登陆了一站式服务大厅。

使用一站式服务大厅

首先给个表格:

应用名称 内部序号(appID)
课表 4770397878132218
成绩 4768574631264620
考试安排 4768687067472349
个人信息 4585275700341858

如何进入应用

  1. 验证是否登录。向 http://ehall.xidian.edu.cn/jsonp/userFavoriteApps.json 发送 GET 请求,查看返回的数据中,hasLogin 是否为 true。如果没登录,则需要登录。登陆需要的 target 是 http://ehall.xidian.edu.cn/login?service=http://ehall.xidian.edu.cn/new/index.html。 一站式登录成功的话,ehall.xidian.edu.cn 下面的 Cookie 会多一个 "MOD_AUTH_CAS=MOD_AUTH_ST-...; path=/;"。
  2. 请求访问应用。向 http://ehall.xidian.edu.cn/appShow 发送 GET 请求,其中请求参数是:
    {"appId": 要访问应用的内部序号}
    header 追加参数如下:
    {"Accept": "text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,image/apng,*/*;q=0.8",}
    
  1. 暂停自动跳转请求,截断下来要跳转的网址,去相应函数请求,如果那个功能必要的话。

提前说明第二发

发送信息中如包含学期信息之情况,按照如此处理:

2022-2023-1 == 2022-2023 学年第一学期

虽然是可以通过请求得到学期数据,不过有的地方,我打算自己算。所以如有对应需求,请查看 xidian-script 代码。

获取成绩(学校想让你看到的)

一会再告诉大家为啥是"学校想让你看到的"。先给大家介绍原理。

获取数据

在获取数据前,还记得那个要跳转的网址吗?你一定要给那个网址发个 GET 请求,要不然,接下来的步骤报错 403 未授权。

实际上这玩意本质上,就是向 http://ehall.xidian.edu.cn/jwapp/sys/cjcx/modules/cjcx/xscjcx.do 发送 POST 请求。不过发送的请求数据,要看情况。

首先说的是一个共有的请求数据,如下:

{
  "name": "SFYX", //是否有效
  "value": "1",
  "linkOpt": "and",
  "builder": "m_value_equal"
}

然后,如果你想查看某个学期的数据,还需要追加以下请求数据:

{
  "name": "XNXQDM", //学年学期代码
  "value": "2022-2023-1",//学期学年代码,参考提前说明第二发
  "linkOpt": "and",
  "builder": "equal"
}

然后,由于是 POST,所以没有请求数据设置(有错请指正)。所以我们附在 POST 报文的数据是:

data: {
  "*json": 1,
  "querySetting": 将上面设置的请求数据编码为 json 字符串,
  "*order": "+XNXQDM,KCH,KXH",
  "pageSize":1000,
  "pageNumber": 1,
},

然后返回的数据,基本上是坑爹的汉语拼音缩写,感觉大家应该会破译吧。毕竟人均黑虎阿福,都是会乌鸦坐飞机的。

关于那些坑爹的拼音缩写

我打开过网页的开发者模式,看到网络菜单有个请求,是向 https://ehall.xidian.edu.cn/jwapp/sys/cjcx/modules/cjcx.do 的 POST 请求。发送的也和上面的头差不多。然后他返回的信息,就是那些坑爹缩写的完整含义。实际上有好多,这里我只列出和查成绩相关的东西。

缩写 含义
XNXQDM 学年学期(代码)
KCM 课程名
KCH 课程号
XSKCM (学生)课程名
XSKCH (学生)课程号
ZCJ 总成绩
KCLBDM 课程类别
KCXZDM 课程性质
XGXKLBDM 校公选课类别
XF 学分
XS 学时
XDFSDM 修读方式
SFZX 修读类型
XFJD 绩点
JDF 积点分(绩点分)
JXBID 教学班id
CXCKDM 重修重考
SFJG 是否及格
SFYX 是否有效

为啥是"学校想让你看到的"

我发现,正常情况下,只有大一新生才能看到自己成绩的排名和具体组成。我查看了页面的源代码,在 ehall.xidian.edu.cn/jwapp/cjcx/*default/modules/cjcx/dqxq/dqxq.js 文件中,有以下几行:

/// The tenth line.
var isDyxs = false;//是否为大一学生(默认为否)西电大一学生可以查看成绩详情
/// The thirty-eighth line and below.
checkIsDyxs : function(){
    isDyxs = false;
    var dqxnxqdm = '';//2017-2018-1
        var xznj = '';//2017
        if("90001" == jwlcgl.getRZLB(roleId)){
            var url = WIS_EMAP_SERV.getAbsPath('/modules/cjcx/cxxsjbxx.do');
            var res = BH_UTILS.doSyncAjax(url, null);
            if(res.code=='0' && res.datas.cxxsjbxx.extParams.code=='1'){
                var xznj = res.datas.cxxsjbxx.extParams.msg;
                if("1" == xznj){
                    isDyxs = true;
                }	
            } else {
                $.bhTip({content: '学生现在年级查询失败,请稍后再试...', state: 'danger'});
                return false;
            }
        }
    },

要想看倒也容易,在网页下载完这个文件但还没加载之前,改掉这俩地方。具体各位可以上网搜"如何在 Chrome 中修改网页代码"。或者使用 Charles 或者 mitproxy 搞中间人拦截,手动改包。

另外说明,这个文件感觉是包含了所有跟查成绩相关的代码,要想深入了解的话可以看看。我对这玩意居然没有加密混淆表示无法理解。

最后我想问的是,这是什么高年级歧视。如果是要”保护“老师的话,不如想想为啥我们学生会对某些老师敬而远之。

获取课表数据

一般来说,获取到的数据是需要处理的。xidian-script 是处理成 iCalender 文件,一个国际上用来处理日历和备忘录的标准格式。我的程序计划是利用 Dart 的一个库,保存成 iCalender 。然后我在网上找到个课表的实现,试着套一下。

当前学期信息

没错,这回我不打算自己合成了:-P

给 http://ehall.xidian.edu.cn/jwapp/sys/wdkb/modules/jshkcb/dqxnxq.do 发送 POST 请求。如果成功的话,在回复数据中的 ['datas']['dqxnxq']['rows'][0]['DM'] 中,就会包含这个学期的数据,格式见上文的提前说明。

感觉其他类似的应用应该有类似的方式来获取目前的学期,或者是所有的学期号码。

获取开学日期

给 http://ehall.xidian.edu.cn/jwapp/sys/wdkb/modules/jshkcb/cxjcs.do 发送 POST 请求,发送的数据如下:

{
    "XN": '学期代码的第一个数字-学期代码的第二个数字', //学年
    "XQ": 学期代码的最后一个数字, // 学期
},

获取的日期字符串在返回数据的 ['datas']['cxjcs']['rows'][0]["XQKSRQ"] 中。

这个东西感觉是为了在显示课表对应日期的时候,找个基准。

获取课表初步数据

给 http://ehall.xidian.edu.cn/jwapp/sys/wdkb/modules/xskcb/xskcb.do 发送 POST 请求,发送数据如下:

{"XNXQDM": 当前学期信息字符串}

然后从 ['datas']['xskcb'] 提取信息。

查看 ['extParams']['code'] 是否为 1。如果是的话,从 ['rows'] 提取数据,否则,从 ['extParams']['msg'] 查看错误信息。

没在课表上的课

要是有门课没有在课表上,我们咋办?

给 https://ehall.xidian.edu.cn/jwapp/sys/wdkb/modules/xskcb/cxxsllsywpk.do 发送 POST 请求,发送数据为:

{"XNXQDM": 当前学期信息字符串}

然后从 ['datas']['cxxsllsywpk'] 提取信息,剩下步骤与上面一致。

金智黑话翻译表

以下多数自己破译,有误请指正。

缩写 含义
KCM 课程名
KCH 课程号
KXH 教学班序号(课序号)
KCLBDM 课程类别
KCXZDM 课程性质
XGXKLBDM 校公选课类别
BJDM 班级
JASDM 上课教室
JXLDM 教学楼
JXBDM 教学班序号
KKDWDM 开课单位
SKJS 授课教师
SKXQ 上课星期
XXXQDM 校区
SKZC 上课周次(是数字数组,对应周的元素代表是否有课)
ZCMC 上课周次(字符串)
KSJC 开始教程
JSJC 结束教程

获取考试安排

先说明一下,这块由于没有基本材料,我目前也没有考试,所以以下逻辑仅供参考。

关于获取学期数据

我经历了那次坑爹的年初疫情,后面我们考试的时候,我们需要自己去一站式更改学期,才能看到我们拖延考试的信息。所以,这里我想做一个查看往学期考试信息的功能。

然后,当我获取所有学期代码的时候,我发现好家伙,数据居然是从 2012 年开始算的。兄弟,我 2020 年入学的好吗?

所以,我感觉每年的二月到七月算春季学期,剩下的是秋季学期,我决定自己获取学期代码。

int now = DateTime.now().month;
String semester = "";
if (now == 1) {
    semester = "${DateTime.now().year-1}-${DateTime.now().year}-1";
} else if (now >= 2 && now <= 7) {
    semester = "${DateTime.now().year-1}-${DateTime.now().year}-2";
} else {
    semester = "${DateTime.now().year}-${DateTime.now().year+1}-1";
}

查询考试安排信息

实际上我找到了三个相关请求,分别是:

缩写 含义
wdksap 我的考试安排
cxyxkwapkwdkc 查询已选课未安排考务的课程
cxwapdksrw 查询未安排的考试任务
我的考试安排

给 https://ehall.xidian.edu.cn/jwapp/sys/studentWdksapApp/modules/wdksap/wdksap.do 发送 POST 请求,发送数据如下:

{
    "XNXQDM":semester,
    "*order":"-KSRQ,-KSSJMS"
}

返回的数据在 [datas][wdksap] 里面,提取之。

如果有考试的话,[extParams][code] 为 1,数据在 [row] 里面。

查询已选课未安排考务的课程

给 https://ehall.xidian.edu.cn/jwapp/sys/studentWdksapApp/modules/wdksap/cxyxkwapkwdkc.do 发送 POST 请求,发送数据如下:

{
    "XNXQDM":semester,
}

返回的数据在 [datas][cxyxkwapkwdkc] 里面,提取之,接下来咋处理我不想说了。

查询未安排的考试任务

这个我每次请求,返回的数据都是"学生查询在考试任务中且没有安排的课程"。所以我也不知道该咋办了,兴许这又是教务处啥不可说的禁区?

金智黑话翻译表

打开网页调试器的网络分项,我看到了请求几个 html 的东西,里面就有缩写和对应中文,这里我摘抄几个。

缩写 含义
KSSJMS 考试时间
KCM 课程名
JASMC 考试地点(具体是啥我也不知道)
ZJJSXM 主讲教师
ZWH 座位号
YKKS 已完成考试
WKKS 未完成考试
BZ 考生须知
YXZXAPKS 院系自行安排

个人信息

感觉这是最没用的功能了,除了打开应用告诉你是谁之外(溜了)

获取方法

还记得获取成绩前,我们需要把跳转网址自行发送 GET 请求吗?获取个人信息时候也需要这么做:-P

如果你需要获取很基本的信息,我们往 http://ehall.xidian.edu.cn/xsfw/sys/swpubapp/userinfo/getConfigUserInfo.do?USERID=学号 发送 POST 请求。数据的 returnCode 里面,有 #E000000000000 就是成功。获取的信息在 [data] 里面,是一个数组。数组元素依次是学号,姓名,学院。

要想获取更加具体的信息,请往 http://ehall.xidian.edu.cn/xsfw/sys/jbxxapp/modules/infoStudent/getStuBatchInfo.do 发送信息,附带数据如下:

{"requestParamStr": 你的学号}

金智黑话翻译表

缩写 含义
XM 姓名
XBDM_DISPLAY 性别
BZ5_DISPLAY 书院
DZ_DWDM_DISPLAY 学院
ZYDM_DISPLAY 专业
ZSDZ 宿舍

总结

感觉看了那么多东西,算是把网页请求和 Cookie 啥的了解了一下,保证将来计网学的时候没有陌生感(虽然据说不咋讲)。

希望那玩意能写出来吧,免得将来找工作说没有编程经验,虽然 Dart 和 Flutter 也是很小众就是了。

SuperBart 2022-8-31

返回老版首页
返回新版首页